动态代理

代理设计模式的原理

使用一个代理对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上。

动态代理的实现

  • ArithmeticCalculator.java

    1
    2
    3
    4
    5
    6
    7
    8
    package com.glemontree.spring.aop.helloworld;
    public interface ArithmeticCalculator {
    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);
    }
  • ArithmeticCalculatorImpl.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.glemontree.spring.aop.helloworld;
    public class ArithmeticCalculatorImpl implements ArithmeticCalculator{
    public int add(int i, int j) {
    int result = i + j;
    return result;
    }
    public int sub(int i, int j) {
    int result = i - j;
    return result;
    }
    public int mul(int i, int j) {
    int result = i * j;
    return result;
    }
    public int div(int i, int j) {
    int result = i / j;
    return result;
    }
    }

    ArithmeticCalculatorImplArithmeticCalculator的一个实现类,其并没有加上日志功能。

  • ArithmeticCalculatorLoggingProxy.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.glemontree.spring.aop.helloworld;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.Arrays;
    public class ArithmeticCalculatorLoggingProxy {
    private ArithmeticCalculator target;
    public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target) {
    this.target = target;
    }
    public ArithmeticCalculator getLoggingProxy() {
    ArithmeticCalculator proxy = null;
    // 代理对象由哪一个类加载器进行加载
    ClassLoader loader = target.getClass().getClassLoader();
    // 代理对象的类型,即其中有哪些方法
    Class[] interfaces = new Class[]{ArithmeticCalculator.class};
    // 当调用代理对象其中的方法时,该执行的代码
    InvocationHandler h = new InvocationHandler() {
    /**
    * proxy: 正在返回的代理对象,一般情况下在invoke方法中都不使用该对象
    * method:正在被调用的方法
    * args:调用方法时传入的参数
    *
    */
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    String methodName = method.getName();
    // 日志
    System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    // 执行方法
    Object result = method.invoke(target, args);
    // 日志
    System.out.println("The method " + methodName + " ends with " + result);
    return result;
    }
    };
    proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
    return proxy;
    }
    }

    ArithmeticCalculatorLoggingProxyArithmeticCalculatorImpl的代理类,其含有一个ArithmeticCalculator类型的对象,且需要在构造函数中进行初始化。

    ArithmeticCalculatorLoggingProxy类中通过getLoggingProxy()方法返回代理对象,代理对象的创建通过Proxy.newProxyInstance()方法进行创建,代理对象的创建不同于普通对象的创建,普通对象一般都是通过new进行创建,代理对象必须通过Proxy.newProxyInstance()方法进行创建。

    Proxy.newProxyInstance()方法需要三个参数,分别是:

    • loader
    • interfaces
    • h

    loader顾名思义表示加载器,也就是代理对象由哪一个类加载器进行加载,这里loader是这样创建的:

    1
    ClassLoader loader = target.getClass().getClassLoader();

    interfaces表示代理对象的类型,即其中有哪些方法,interfaces是这样创建的:

    1
    Class[] interfaces = new Class[]{ArithmeticCalculator.class};

    h表示当调用代理对象其中的方法时,该执行的代码,这里是这样创建的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    InvocationHandler h = new InvocationHandler() {
    /**
    * proxy: 正在返回的代理对象,一般情况下在invoke方法中都不使用该对象
    * method:正在被调用的方法
    * args:调用方法时传入的参数
    *
    */
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    String methodName = method.getName();
    // 日志
    System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    // 执行方法
    Object result = method.invoke(target, args);
    // 日志
    System.out.println("The method " + methodName + " ends with " + result);
    return result;
    }
    };

    h的类型为InvocationHandler类型,在创建h时需要实现其public Object invoke(Object proxy, Method method, Object[] args)方法,该方法有三个参数:

    • proxy:正在返回的代理对象,一般情况下在invoke方法中都不使用该对象
    • method:正在被调用的方法
    • args:调用方法时传入的参数

    当使用代理对象调用方法时,会转而调用该方法,在该方法中可以加上自己需要的日志信息或其他信息,当然在该方法中还需要调用真正的需要调用的方法,这是通过下面这行代码实现的:Object result = method.invoke(target, args);,最后将result返回即可。

  • Main.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.glemontree.spring.aop.helloworld;
    public class Main {
    public static void main(String[] args) {
    /*ArithmeticCalculator arithmeticCalculator = null;
    arithmeticCalculator = new ArithmeticCalculatorLoggingImpl();
    int result = arithmeticCalculator.add(1, 2);
    System.out.println("-->" + result);
    result = arithmeticCalculator.div(4, 2);
    System.out.println("-->" + result);*/
    ArithmeticCalculator target = new ArithmeticCalculatorImpl();
    ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();
    int result = proxy.add(1, 2);
    System.out.println("-->" + result);
    result = proxy.div(4, 2);
    System.out.println("-->" + result);
    }
    }

AOP简介

AOP(Aspect-Oriented Programming,面向切面编程)是一种新的方法论,是对传统OOP的补充!

AOP的好处:

  • 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
  • 业务模块更简洁,只包含核心业务代码

AOP图解

  • 切面:横切关注点(跨越应用程序多个模块的功能),被模块化的特殊对象
  • 通知:切面必须要完成的工作
  • 目标:被通知的对象
  • 代理:向目标对象应用通知之后创建的对象
  • 连接点:程序执行的某个特定位置,如类中某个方法调用前、调用后、方法抛出异常后等,连接点由两个信息确定,方法表示的程序执行点,相对点表示的方位
  • 切点:每个类都拥有多个连接点,例如ArithmethicCalculator的所有方法都是连接点,即连接点是程序类中客观存在的事务,AOP通过切点定位到特定的连接点,类比:连接点相当于数据库中的记录,切点相当于查询条件,切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。

Spring AOP前置通知

  • AspectJ:java社区里最完整最流行的AOP框架

  • 可以使用基于AspectJ注解或基于XML配置的AOP

  • 使用Spring AOP

  • 加入jar包:

    • com.springsource.net.sf.cglib-2.2.0.jar
    • com.springsource.org.aopalliance-1.0.0.jar
    • com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
    • commons-logging-1.1.1.jar
    • spring-aop-4.0.0.RELEASE.jar
    • spring-aspects-4.0.0.RELEASE.jar
    • spring-beans-4.0.0.RELEASE.jar
    • spring-context-4.0.0.RELEASE.jar
    • spring-core-4.0.0.RELEASE.jar
    • spring-expression-4.0.0.RELEASE.jar
  • 在配置文件中加入aop的命名空间:

    xmlns:aop=”http://www.springframework.org/schema/aop

  • 基于注解的方式:

    • 在配置文件中加入如下配置:

      1
      <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

      这个注解的作用是使Aspect注解起作用:自动为匹配的类生成代理对象

    • 把横切关注点的代码抽象到切面的类中

    • 切面的类首先是一个IOC容器中bean,并加入@Component注解

    • 切面还需要加入@Aspect注解

    • 在类中声明各种通知

      常见的有5种通知,分别是:

    • @Before:前置通知,在方法执行之前执行

    • @After:后置通知,在方法执行之后执行
    • @AfterRunning:返回通知,在方法返回结果之后执行
    • @AfterThrowing:异常通知,在方法抛出异常之后
    • @Around:环绕通知,围绕着方法执行

      声明通知其实就是声明方法:

    • 声明一个方法

    • 在方法前加入@Before注解
    • 可以在声明方法中声明一个类型为JointPoint类型的参数,然后就能访问链接细节,如方法名称和参数值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.glemontree.spring.aop.impl;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    // 把这个类声明为一个切面:需要把该类放入到IOC容器中,再声明为一个切面
    @Aspect
    @Component
    public class LoggingAspect {
    //声明该方法为一个前置通知:在目标方法开始执行之前执行
    @Before("execution(* com.glemontree.spring.aop.impl.*.*(int, int))")
    public void beforeMethod(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    List<Object> args = Arrays.asList(joinPoint.getArgs());
    System.out.println("The method " + methodName + " begins with " + args);
    }
    }

后置通知

和前置通知很类似,后置通知使用@After注解:

1
2
3
4
5
@After("execution(* com.glemontree.spring.aop.impl.*.*(int, int))")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}

需要注意的是后置通知是在目标方法执行之后执行,其不管是否会发生异常都会执行。另外需要注意后置通知中不能访问目标方法执行的结果。

返回通知

1
2
3
4
5
6
// 在方法正常执行后执行的代码,返回通知是可以访问到方法的返回值的
@AfterReturning(value="execution(* com.glemontree.spring.aop.impl.*.*(int, int))", returning="result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends with " + result);
}

异常通知

1
2
3
4
5
6
7
8
/**
* 在目标方法出现异常时会执行的代码,可以访问到异常对象且可以指定在出现特定异常时再执行通知代码
*/
@AfterThrowing(value="execution(* com.glemontree.spring.aop.impl.*.*(int, int))", throwing="ex")
public void afterThrowing(JoinPoint joinPoint, NullPointerException ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs exception: " + ex);
}

环绕通知

环绕通知类似于动态代理,虽然功能很强大,但是用的不多,这里不加以介绍!

切面的优先级

可以通过@Order注解指定切面的优先级,值越小优先级越高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.glemontree.spring.aop.impl;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Component
@Aspect
public class ValidationAspect {
@Before("execution(* com.glemontree.spring.aop.impl.*.*(int, int))")
public void validateArgs(JoinPoint joinPoint) {
System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
}
}

重用切点表达式

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个方法,用于声明切入点表达式,一般的,该方法中不需要填入其他的代码
@Pointcut("execution(* com.glemontree.spring.aop.impl.*.*(int, int))")
public void declareJointPointExpression() {
}
//声明该方法为一个前置通知:在目标方法开始执行之前执行
@Before("declareJointPointExpression()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method " + methodName + " begins with " + args);
}

对于同一个包下的另一个切面,可以使用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.glemontree.spring.aop.impl;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Component
@Aspect
public class ValidationAspect {
@Before("LoggingAspect.declareJointPointExpression()")
public void validateArgs(JoinPoint joinPoint) {
System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
}
}

当然,对于不同包下的切面,可能还需要使用包名。